package org.example;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HTMLMetroParser {
    private static Pattern patternStationConnected = Pattern.compile(".*«(.+)».*");
    private static Pattern patternLineConnected = Pattern.compile(".* ln-(.+)$");

    private final List<MetroLine> metroLines = new ArrayList<>();
    private final List<MetroStation> metroStations = new ArrayList<>();

    // Indexes for quick search
    private final Map<String, MetroLine> metroLinesIndex = new HashMap<>();           // Key is line number
    private final Map<String, MetroStation> metroStationsIndex = new HashMap<>();     // Key is station id

    HTMLMetroParser(Document document) throws DataFormatException {
        parse(document);
    }

    public static Document getDocumentByUrl(String url) throws IOException {
        return Jsoup.connect(url).get();
    }

    public void clear() {
        metroLines.clear();
        metroLinesIndex.clear();

        metroStations.clear();
        metroStationsIndex.clear();
    }

    private void parse(Document document) throws DataFormatException {
        clear();

        parseConnections(
                parseStations(document,
                        parseLines(document)
                )
        );
    }

    private Set<String> parseLines(Document document) throws DataFormatException {
        Set<String> lineNumbers = new HashSet<>();

        Elements lines = document.select("span.js-metro-line");

        for (Element element : lines) {
            MetroLine metroLine = new MetroLine(
                    element.attributes().get("data-line"),  // Line number
                    element.text()                          // Line name
            );

            if (!lineNumbers.add(metroLine.getNumber())) {
                clear();
                throw new DataFormatException("Several lines of number " + metroLine.getNumber());
            }

            metroLines.add(metroLine);
            metroLinesIndex.put(metroLine.getNumber(), metroLine);
        }

        return lineNumbers;
    }

    private Map<String, List<String>> parseStations(Document document, Set<String> lineNumbers) throws DataFormatException {
        Map<String, List<String>> connections = new HashMap<>();

        Elements stations = document.select("p.single-station");

        for (Element element : stations) {
            try {
                MetroStation metroStation = new MetroStation(
                        element.child(1).text(),                    // Station name
                        element.parent().attributes().get("data-line")    // Line number
                );

                if (!lineNumbers.contains(metroStation.getLineNumber())) {
                    clear();
                    throw new DataFormatException("Station of unknown line number " + metroStation.getID());
                }

                List<String> stationConnections = parseStationConnections(element);

                if (!stationConnections.isEmpty()) {
                    connections.put(metroStation.getID(), stationConnections);
                }

                metroStations.add(metroStation);
                metroStationsIndex.put(metroStation.getID(), metroStation);
            } catch (NullPointerException e) {}
        }

        return connections;
    }

    private List<String> parseStationConnections(Element element) {
        List<String> stationConnections = new ArrayList<>();

        try {
            for (int i = 2; ; i++) {
                Element child = element.child(i);

                Matcher stationMatcher = patternStationConnected.matcher(
                        child.attributes().get("title")
                );
                Matcher lineMatcher = patternLineConnected.matcher(
                        child.attributes().get("class")
                );

                if (stationMatcher.find() && lineMatcher.find()) {
                    String station = stationMatcher.group(1);
                    String line = lineMatcher.group(1);

                    if (station != null && line != null) {
                        stationConnections.add(MetroStation.getID(station, line));
                    }
                }
            }
        } catch (IndexOutOfBoundsException e) {}

        return stationConnections;
    }

    private void parseConnections(Map<String, List<String>> connections) throws DataFormatException {
        for (Map.Entry<String, List<String>> stationConnections : connections.entrySet()) {
            MetroStation station = getMetroStation(stationConnections.getKey());

            for (String connectedStationId : stationConnections.getValue()) {
                MetroStation connectedStation = getMetroStation(connectedStationId);

                if (connectedStation == null) {
                    clear();
                    throw new DataFormatException("Connected station not exists " + connectedStationId);
                }

                station.addConnection(connectedStation);
            }
        }

        for (MetroStation station : metroStations) {
            for (MetroStation connectedStation : station.getConnections()) {
                if (!connectedStation.isConnected(station)) {
                    clear();
                    throw new DataFormatException("One way connection " + station.getID() + " to " + connectedStation.getID());
                }
            }
        }
    }

    public Collection<MetroLine> getMetroLines() {
        return new ArrayList<>(metroLines);
    }

    public Collection<MetroStation> getMetroStations() {
        return new ArrayList<>(metroStations);
    }

    public MetroLine getMetroLine(String number) {
        return metroLinesIndex.get(number);
    }

    public MetroStation getMetroStation(String id) {
        return metroStationsIndex.get(id);
    }

    public String toString() {
        StringJoiner stringJoiner = new StringJoiner(System.lineSeparator());

        for (MetroLine metroLine : getMetroLines()) {
            stringJoiner.add(metroLine.toString());

            for (MetroStation metroStation : getMetroStations()) {
                if (metroStation.getLineNumber().equals(metroLine.getNumber())) {
                    stringJoiner.add("\t" + metroStation.toString().replace(
                            System.lineSeparator(),
                            System.lineSeparator() + "\t"
                    ));
                }
            }
        }

        return stringJoiner.toString();
    }
}
